Read Buf

Read Buf

如何在 C(在 Linux 中)使用新的 counted_by 属性

developer

pic

counted_by 属性

counted_by 属性首次在 Clang-18 中引入,并即将在 GCC-15 中使用。它的主要功能是将一个 flexible-array 成员 与一个结构成员关联起来,这个结构成员在运行时会保存该数组中的元素数量。这对于通过 array bounds sanitizer__builtin_dynamic_object_size() 内置函数进行运行时边界检查非常重要。在用户空间中,可以通过 -D_FORTIFY_SOURCE=3 启用这种额外的安全机制。因此,正确使用此属性可以增强 C 代码库中 flexible-array 成员的运行时边界检查。

以下是一个带有此属性的 flexible array 示例:

struct bounded_flex_struct {
        ...
        size_t count;
        struct foo flex_array[] __attribute__((__counted_by__(count)));
};

在这个示例中,count 是用于保存 flexible array 元素数量的结构成员,我们称之为计数器。

在 Linux 内核中,这一属性通过增强的 API(如 memcpy() 系列函数)实现边界检查,这些 API 内部调用了 __builtin_dynamic_object_size()(CONFIG_FORTIFY_SOURCE)。此外,这一属性还通过数组边界检查器(CONFIG_UBSAN_BOUNDS)实现。

__counted_by() 宏

在内核中,我们将 counted_by 属性封装在 __counted_by() 宏中,代码如下:

#if __has_attribute(__counted_by__)
# define __counted_by(member)  __attribute__((__counted_by__(member)))
#else
# define __counted_by(member)
#endif
  • c8248faf3ca27 (“编译器属性:counted_by:调整名称…”)

通过这个宏,我们在过去的一年里在整个内核代码树中为 flexible-array 成员添加了注释。

diff --git a/drivers/net/ethernet/chelsio/cxgb4/sched.h b/drivers/net/ethernet/chelsio/cxgb4/sched.h
index 5f8b871d79afac..6b3c778815f09e 100644
--- a/drivers/net/ethernet/chelsio/cxgb4/sched.h
+++ b/drivers/net/ethernet/chelsio/cxgb4/sched.h
@@ -82,7 +82,7 @@ struct sched_class {
 
 struct sched_table {      /* 每个端口调度表 */
 	u8 sched_size;
-	struct sched_class tab[];
+	struct sched_class tab[] __counted_by(sched_size);
};
  • ceba9725fb45 (“cxgb4: 为 struct sched_table 添加注释…”)

不过,并不是所有的 __counted_by() 注释都像上面的示例那样简单。

内核中的 __counted_by() 注释

要正确使用 counted_by 属性,有一些关键要求。首先,计数器必须在第一次引用 flexible-array 成员前初始化。其次,数组中的元素数量应当至少与计数器指示的数量相等。以下是一个满足这些要求的内核补丁示例:

diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c
index dac7eb77799bd1..68960ae9898713 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/fweh.c
@@-33,7 +33,7 @@ struct brcmf_fweh_queue_item {
 u8 ifaddr[ETH_ALEN];
 struct brcmf_event_msg_be emsg;
 u32 datalen;
- u8 data[];
+ u8 data[] __counted_by(datalen);
};
 
/*
@@-418,17 +418,17 @@ void brcmf_fweh_process_event(struct brcmf_pub *drvr,
 datalen + sizeof(*event_packet) > packet_len)
	return;
 
- event = kzalloc(sizeof(*event) + datalen, gfp);
+ event = kzalloc(struct_size(event, data, datalen), gfp);
 if (!event)
	return;
 
+ event->datalen = datalen;
 event->code = code;
 event->ifidx = event_packet->msg.ifidx;
 
 /* 使用 memcpy 以获取对齐的事件消息 */
 memcpy(&event->emsg, &event_packet->msg, sizeof(event->emsg));
 memcpy(event->data, data, datalen);
- event->datalen = datalen;
 memcpy(event->ifaddr, event_packet->eth.h_dest, ETH_ALEN);
 
 brcmf_fweh_queue_event(fweh, event);
  • 62d19b358088 (“wifi: brcmfmac: fweh: 添加 __counted_by…“)

在上述补丁中,datalen 是 flexible-array 成员 data 的计数器。注意到将 event->datalen = datalen 的赋值操作移到调用 memcpy(event->data, data, datalen) 之前,确保在首次引用 flexible-array 前初始化计数器。否则,编译器会因试图写入大小为零的 flexible-array 而报错,因为 datalen 被之前的 kzalloc() 调用清零了。这种在调用 memcpy 后再进行赋值的模式在 Linux 内核中很常见,但在处理 counted_by 注释时应当改变。因此,我们需要小心处理这些注释,确保代码满足正确的要求。

在内核中,我们从错误中学习,并修复了一些最初我们做的错误注释。以下是一些错误修复示例:

  • 6dc445c19050 (“clk: bcm: rpi: 在访问前赋值 –>num…”)
  • 9368cdf90f52 (“clk: bcm: dvp: 在访问前赋值 –>num…”)

另一个常见的问题是计数器在循环内更新。请参阅以下补丁:

diff --git a/drivers/net/wireless/ath/wil6210/cfg80211.c b/drivers/net/wireless/ath/wil6210/cfg80211.c
index 8993028709ecfb..e8f1d30a8d73c5 100644
--- a/drivers/net/wireless/ath/wil6210/cfg80211.c
+++ b/drivers/net/wireless/ath/wil6210/cfg80211.c
@@-892,10 +892,8 @@ static int wil_cfg80211_scan(struct wiphy *wiphy,
 struct wil6210_priv *wil = wiphy_to_wil(wiphy);
 struct wireless_dev *wdev = request->wdev;
 struct wil6210_vif *vif = wdev_to_vif(wil, wdev);
- struct {
- struct wmi_start_scan_cmd cmd;
- u16 chnl[4];
- } __packed cmd;
+ DEFINE_FLEX(struct wmi_start_scan_cmd, cmd,
+ channel_list, num_channels, 4);
 uint i, n;
 int rc;
 
@@-977,9 +975,8 @@ static int wil_cfg80211_scan(struct wiphy *wiphy,
 vif->scan_request = request;
 mod_timer(&vif->scan_timer, jiffies + WIL6210_SCAN_TO);
 
- memset(&cmd, 0, sizeof(cmd));
- cmd.cmd.scan_type = WMI_ACTIVE_SCAN;
- cmd.cmd.num_channels = 0;
+ cmd->scan_type = WMI_ACTIVE_SCAN;
+ cmd->num_channels = 0;
 n = min(request->n_channels, 4U);
 for (i = 0; i < n; i++) {
 int ch = request->channels[i]->hw_value;
@@-991,7 +988,8 @@ static int wil_cfg80211_scan(struct wiphy *wiphy,
 continue;
 }
 /* 0-based channel indexes */
- cmd.cmd.channel_list[cmd.cmd.num_channels++].channel = ch - 1;
+ cmd->num_channels++;
+ cmd->channel_list[cmd->num_channels - 1].channel = ch - 1;
 wil_dbg_misc(wil, "Scan for ch %d : %d MHz\n", ch,
 request->channels[i]->center_freq);
 }
...
--- a/drivers/net/wireless/ath/wil6210/wmi.h
+++ b/drivers/net/wireless/ath/wil6210/wmi.h
@@-474,7 +474,7 @@ struct wmi_start_scan_cmd {
 struct {
 u8 channel;
 u8 reserved;
- } channel_list[];
+ } channel_list[] __counted_by(num_channels);
} __packed;
  • 34c34c242a1b (“wifi: wil6210: cfg80211: 使用 __counted_by…”)

如补丁所示,问题在于当 num_channels 为零时,cmd.cmd.channel_list[cmd.cmd.num_channels++] 会导致未定义行为并可能触发编译器警告。解决方案是在访问数组之前增加 num_channels,然后通过索引访问数组。

另一种选择是完全避免将计数器用作 flexible-array 的索引,可以通过引入辅助变量实现。以下是一个补丁片段:

diff --git a/include/net/bluetooth/hci.h b/include/net/bluetooth/hci.h
index 38eb7ec86a1a65..21ebd70f3dcc97 100644
--- a/include/net/bluetooth/hci.h
+++ b/include/net/bluetooth/hci.h
@@-2143,7 +2143,7 @@ struct hci_cp_le_set_cig_params {
 __le16 c_latency;
 __le16 p_latency;
 __u8 num_cis;
- struct hci_cis_params cis[];
+ struct hci_cis_params cis[] __counted_by(num_cis);
} __packed;

@@-1722,34 +1717,33 @@ static int hci_le_create_big(struct hci_conn *conn, struct bt_iso_qos *qos)
 static int set_cig_params_sync(struct hci_dev *hdev, void *data)
 {
...

+ u8 aux_num_cis = 0;
 u8 cis_id;
...

 for (cis_id = 0x00; cis_id < 0xf0 &&
- pdu.cp.num_cis < ARRAY_SIZE(pdu.cis); cis_id++) {
+ aux_num_cis < pdu->num_cis; cis_id++) {
 struct hci_cis_params *cis;

conn = hci_conn_hash_lookup_cis(hdev, NULL, 0, cig_id, cis_id);
@@-1758,7 +1752,7 @@ static int set_cig_params_sync(struct hci_dev *hdev, void *data)

 qos = &conn->iso_qos;

- cis = &pdu.cis[pdu.cp.num_cis++];
+ cis = &pdu->cis[aux_num_cis++];
 cis->cis_id = cis_id;
 cis->c_sdu = cpu_to_le16(conn->iso_qos.ucast.out.sdu);
 cis->p_sdu = cpu_to_le16(conn->iso_qos.ucast.in.sdu);
@@-1769,14 +1763,14 @@ static int set_cig_params_sync(struct hci_dev *hdev, void *data)
 cis->c_rtn = qos->ucast.out.rtn;
 cis->p_rtn = qos->ucast.in.rtn;
}
+ pdu->num_cis = aux_num_cis;

...
  • ea9e148c803b (“Bluetooth: hci_conn: 使用 __counted_by()…”)

在这个示例中,新的辅助变量 aux_num_cis 被引入以替代计数器 num_cis 进行数组访问:&pdu->cis[aux_num_cis++]。计数器在循环之后更新:pdu->num_cis = aux_num_cis

两种解决方案都可以接受,根据需要选择适合的方式。

以下是最近一些错误修复,用以修正没有注意到上述细节的注释:

  • PATCH “wifi: iwlwifi: mvm: 修复 _counted_by 用法在 cfg80211_wowlan_nd* 中的错误”

参考链接